package jannex;

import jannex.internal.DriverManagerDataSource;
import jannex.internal.SqlTypeUtils;
import jannex.label.LabelMap;
import jannex.mapping.*;

import javax.sql.DataSource;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.util.*;

public class JannexBuilder implements Builder<Jannex> {

    private DataSource dataSource;

    private final Map<Class<?>, Mapping<?>> mappings = new HashMap<>();
    private final Map<Class<?>, ResultSetHandler<?>> handlers = new HashMap<>();
    private final Map<Class<?>, Builder<?>> objectBuilders = new HashMap<>();

    private LabelMap.Builder<Class<?>> customTypes = LabelMap.builder();

    private String defaultSchema = "";

    private boolean excludeFieldsWithoutColumn;
    private boolean excludeMethods;
    private int excludeFieldModifiers = Modifier.TRANSIENT | Modifier.STATIC;
    private final List<ExclusionStrategy> exclusionStrategies = new ArrayList<>();
    private boolean statelessMapping;

    private String url;
    private String username;
    private String password;

    private Map<String, Class<?>> defaultTypes;

    public JannexBuilder(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public JannexBuilder(String driverUrl) {
        this.url = driverUrl;
    }

    public JannexBuilder excludeFieldsWithoutColumn(boolean excludeFieldsWithoutColumn) {
        this.excludeFieldsWithoutColumn = excludeFieldsWithoutColumn;
        return this;
    }

    public JannexBuilder excludeFieldsWithModifiers(int modifiers) {
        this.excludeFieldModifiers = modifiers;
        return this;
    }

    public JannexBuilder excludeMethods(boolean excludeMethods) {
        this.excludeMethods = excludeMethods;
        return this;
    }

    public JannexBuilder defaultSchema(String defaultSchema) {
        this.defaultSchema = defaultSchema;
        return this;
    }

    public JannexBuilder credentials(String username, String password) {
        this.username = username;
        this.password = password;
        return this;
    }

    @SuppressWarnings("unchecked")
    public <E> Binder<E> binder(Class<E> elementClass) {
        return new Binder<>(this, mappingFactory(), resultSetFactory(), elementClass)
                .handler((ResultSetHandler<E>) handlers.get(elementClass))
                .objectBuilder((Builder<E>) objectBuilders.get(elementClass))
                .schema(defaultSchema);
    }

    public <E> JannexBuilder addBuilder(Class<E> elementClass, Builder<E> objectBuilder) {
        if (objectBuilders.containsKey(elementClass)) {
            throw new JannexException("Object builder "
                    + " already registered for type " + elementClass.getName());
        }
        objectBuilders.put(elementClass, objectBuilder);
        return this;
    }

    public <E> JannexBuilder addHandler(Class<E> elementClass, ResultSetHandler<E> handler) {
        if (handlers.containsKey(elementClass)) {
            throw new JannexException(ResultSetHandler.class.getSimpleName()
                    + " already registered for type " + elementClass.getName());
        }
        handlers.put(elementClass, handler);
        return this;
    }

    public <E> JannexBuilder addMapping(Class<E> elementClass, Mapping<E> mapping) {
        if (mappings.containsKey(elementClass)) {
            throw new JannexException(Mapping.class.getSimpleName()
                    + " already registered for type " + elementClass.getName());
        }
        mappings.put(elementClass, mapping);
        return this;
    }

    public JannexBuilder addExclusionStrategy(ExclusionStrategy exclusionStrategy) {
        this.exclusionStrategies.add(exclusionStrategy);
        return this;
    }

    public JannexBuilder statelessMapping(boolean statelessMapping) {
        this.statelessMapping = statelessMapping;
        return this;
    }

    public JannexBuilder addTypeMapping(String typeName, Class<?> typeClass) {
        if (customTypes == null) {
            customTypes = LabelMap.builder();
        }
        customTypes.put(typeName, typeClass);
        return this;
    }

    @Override
    public Jannex build() {
        if (dataSource == null) {
            dataSource = new DriverManagerDataSource(url, username, password);
        }
        Map<String, Class<?>> types = null;
        if (customTypes != null) {
            Map<String, Class<?>> defaultTypeMap;
            try (Connection connection = dataSource.getConnection()) {
                defaultTypeMap = connection.getTypeMap();
            } catch (Exception e) {
                defaultTypeMap = SqlTypeUtils.getDefaultTypeMap();
            }
            types = LabelMap.<Class<?>>builder()
                    .putAll(customTypes.build())
                    .put("LIST", ArrayList.class)
                    .put("SET", HashSet.class)
                    .put("COLLECTION", ArrayList.class)
                    .putAll(defaultTypeMap).build();
        }

        return new Jannex(dataSource, username, password, mappingContext(), types);
    }

    private MappingFactory mappingFactory() {
        return new MappingFactory(exclusionStrategies, excludeFieldsWithoutColumn, defaultSchema,
                excludeMethods, excludeFieldModifiers, defaultTypes);
    }

    private Map<String, Class<?>> buildTypes() {
        Map<String, Class<?>> types = null;
        if (customTypes != null) {
            if (defaultTypes == null) {
                try (Connection connection = dataSource.getConnection()) {
                    defaultTypes = connection.getTypeMap();
                } catch (Exception e) {
                    defaultTypes = SqlTypeUtils.getDefaultTypeMap();
                }
            }
            types = LabelMap.<Class<?>>builder()
                    .putAll(customTypes.build())
                    .put("LIST", ArrayList.class)
                    .put("SET", HashSet.class)
                    .put("COLLECTION", ArrayList.class)
                    .putAll(defaultTypes).build();
        }
        return types;
    }

    private ResultSetHandlerFactory resultSetFactory() {
        return new ResultSetHandlerFactory(mappingContext());
    }

    private MappingContext mappingContext() {
        return new MappingContext(mappingFactory(), statelessMapping);
    }
}
